class Txt_Source(object):
    '''An object designed to traverse a sting I/O'''
    
    
    def __init__(self, path):
        '''Create a new txt_source object of given path.'''
        #initialize reader
        self.reader = open(path, "r")
        #initialize current line variable
        self.line = None
        #initialize line archive
        self.archive = []
    
    
    def next_line(self):
        '''(Txt_Source)->String
        Return the next line of the reader.'''
        #store the current line
        self.line = self.reader.readline()
        #archive the line
        self.archive.append(self.line)        
        return self.line
    
    
    def get_curr(self):
        '''(Txt_Source)->String
        Return the current line.'''
        return self.line
    
    
    def get_prev(self, i=2):
        '''(Txt_Source, int)->String
        Return the archived string at position i from the end of the archive.
        Default to most recent addition'''
        return self.archive[-i]
    
    
    def read_until(self, stop_string, run_cap=None):
        '''(Txt_Source, String, int)->String
        Return the next line containing stop_string. If given, read a maximum
        of run_cap number of lines'''
        count = 0
        line = self.next_line()
        while stop_string not in line and count != run_cap:
            line = self.next_line()
            count += 1
        return line
    
    
    def get_line(self, n):
        '''(Txt_Source, int)->String
        Return the line at index n in the archive.'''
        return self.archive[n]
    
    
    def find_line(self, s, i=0):
        '''(Txt_Source, String)->int
        Return the first index after given int i 
        containing the given string s'''
        while s not in self.archive[i]:
            i += 1
        return i
    
    
    def archive_len(self):
        return len(self.archive)
    
    
    def done_reading(self):
        self.reader.close()



class Program(object):
    
    
    def __init__(self, name=None, page=None):
        self.name = name
        self.page = page
        self.courses = {}


    def read_course(self, lines, i):
        '''(program, int)->(int)
        Create a course object in the courses dictionary and return the 
        index of the line follwing the course
        '''
        def get_course_name(lines, i, c):
            start_i = i
            #get the line
            start_line = lines[start_i]
            #trim the code from the line
            name = [start_line[len(c.get_code()):].strip()]
            #collate the name from the following lines
            line = start_line
            while line[-1] != ']':
                i += 1
                line = lines[i]
                if 'BR=' not in line and 'DR=' not in line:
                    name.append(line)
                #if the reader gets to the br or dr,
                #then it is not a commonly formatted title
                else:
                    return (start_line[len(c.get_code()):].strip(), start_i + 1)
            #in the normal case, return the joined lines
            return (' '.join(name), i + 1)
        
        c = Course()

        line = lines[i]
        split_line = line.split()
        #find and set the code
        c.set_code(split_line[0])
        #get course name
        name, i = get_course_name(lines, i, c)
        c.set_name(name)
        line = lines[i]
        desc = ''
        #collate the description until prereqs exclusions or DR/BRs are found
        while 'Prerequisite:' not in line and\
              'Recommended Preparation:' not in line and\
              'Exclusion:' not in line and\
              'DR=' not in line and\
              'BR=' not in line:
            desc = desc + '\n' + line
            i += 1
            line = lines[i]
        c.set_desc(desc.strip())
        #set prerequisite if it exists
        if 'Prerequisite:' in line:
            #collect all lines of the prerequisite
            prereq = line[13:].strip()
            i += 1
            line = lines[i]
            while 'Recommended Preparation:' not in line and\
                  'Exclusion:' not in line and\
                  'DR=' not in line and\
                  'BR=' not in line:
                prereq = prereq + '\n' + line
                i += 1
                line = lines[i]           
            c.set_pre(prereq.strip())
        #set exclusion if it exists
        if 'Exclusion:' in line:
            excl = line[10:].strip()
            i += 1
            line = lines[i]
            while 'Recommended Preparation:' not in line and\
                  'DR=' not in line and\
                  'BR=' not in line:
                excl = excl + '\n' + line
                i += 1
                line = lines[i]            
            c.set_exc(excl.strip())
        #set the Recommended Preparation if it exists
        if 'Recommended Preparation:' in line:
            rec_prep = line[24:]
            i += 1
            line = lines[i]
            while 'DR=' not in line and\
                  'BR=' not in line:
                rec_prep = rec_prep + '\n' + line
                i += 1
                line = lines[i]
            c.set_rec_prep(rec_prep.strip())
        #the final line is the DR and BR
        if 'DR=' in line:
            req_split = line.find(';')
            dr_val = line[3:req_split].strip()
            c.set_dr(dr_val)
            line = line[req_split + 1:].strip()
        if 'BR=' in line:
            br_val = line[3:].strip()
            c.set_br(br_val)
        
        self.courses[c.get_code()] = c
        
        return i + 1

        
        
class Course(object):
    
    def __init__(self):
        self.code = None
        self.name = None
        self.desc = None
        self.pre = None
        self.rec_prep = None
        self.exc = None
        self.dr = None
        self.br = None
        
    def __str__(self):
        s = '%s\n\n%s\n' %(self.name, self.desc)
        if self.pre:
            s = '%sPrerequisite: %s\n' %(s, self.pre)
        if self.rec_prep:
            s = '%sRecommended Preparation: %s\n' %(s, self.rec_prep)
        if self.exc:
            s = '%sExclusion: %s\n' %(s, self.exc)
        if self.dr:
            drs = ''
            for i in range(len(self.dr)):
                drs = '%s%s+' %(drs, self.dr[i])
            s = '%sDR=%s\n' %(s, drs.rstrip('+'))
        if self.br:
            brs = ''
            for i in range(len(self.br)):
                brs = '%s%s/' %(brs, self.br[i])
            s = '%sBR=%s\n' %(s, brs.rstrip('/'))
        return s
        
    def set_code(self, value):
        self.code = value
        
    def get_code(self):
        return self.code
        
    def set_name(self, value):
        self.name = value
        
    def get_name(self):
        return self.name
        
    def set_desc(self, value):
        self.desc = value
        
    def get_desc(self):
        return self.desc
        
    def set_pre(self, value):
        self.pre = value
        
    def get_pre(self):
        return self.pre
    
    def set_rec_prep(self, value):
        self.rec_prep = value
        
    def get_rec_prep(self):
        return self.rec_prep
        
    def set_exc(self, value):
        self.exc = value
        
    def get_exc(self):
        return self.exc
        
    def set_dr(self, value):
        self.dr = value.split('/')

    def get_dr(self):
        return self.dr
        
    def set_br(self, value):
        self.br = value.split('+')
        for i in range(len(self.br)):
            self.br[i] = self.br[i].strip()        
        
    def get_br(self):
        return self.br



class Cal_DB(object):
    
    
    def __init__(self, path=None):
        self.source_path = path
        self.source = Txt_Source(path)
        self.programs = {}
        self.program_txts = {}
        
        
    def populate_progs_list(self, start_string):
        '''(Cal_DB, String)->NoneType
        Populate the programs listing by reading the Table of Contents 
        page starting from given start_string'''
        stop_count = 0
        #get to start point
        self.source.read_until(start_string)
        #run until 5 lines are not program listings
        while stop_count != 5:
            line = self.source.next_line().strip()
            line_part = line.rpartition(' ')
            try:
                int(line_part[2])
                is_num = True
            except ValueError:
                is_num = False            
            if is_num:
                line_split = line.split('.')
                page_num = line_split[-1].strip()
                prog_name = line_split[0].strip()
                #convert "&" to "and" (refer to |2| in notes for reason)
                while '&' in prog_name:
                    i = prog_name.find('&')
                    prog_name = '%sand%s' %(prog_name[:i], prog_name[i + 1:])
                self.programs[prog_name] = Program(prog_name, page_num)
                stop_count = 0
            else:
                stop_count += 1
                
                
    def populate_program_txts(self):
        '''(Cal_DB)->NoneType
        Populate the programs_txt dictionary by reading through
        entire calendar. Requires: Programs list has been populated'''
        program = None
        line = self.source.next_line()
        while line != '':
            #check if line is a program name
            if self.is_header(line.strip()):
                #save program name incase the line is not actually a header
                program_save = program
                program = line.strip()
                #check if next line is a page number
                line = self.source.next_line().strip()
                #if the next line is a number, keep the program change
                try:
                    int(line.strip())
                    #line = self.source.next_line()
                #if not, then reverse the change and append the skipped line
                except ValueError:
                    program = program_save
                    if program:
                        retro_line = self.source.get_prev()
                        self.program_txts[program].append(retro_line)
            elif program:
                    if program in self.program_txts.keys():
                        self.program_txts[program].append(line)
                    else:
                        self.program_txts[program] = [line]
            line = self.source.next_line().strip()
        #close the source reader
        self.source.done_reading()
        
        
    def mine_courses(self, prog_name):
        #a helper function to skip next start line of a course
        def skip_to_next_course(lines, i):
            if i < len(lines):
                line_code = lines[i].split()[0]
            while i < len(lines) and not (len(line_code) == 8 and\
                  (line_code[-1] == '1' or line_code[-1] == '0')):
                i += 1
                if i < len(lines):
                    line_code = lines[i].split()[0]
            return i
                
        program = self.programs[prog_name]
        lines = self.program_txts[prog_name]
        i = 0
        #find the index where the course listing starts
        s = "%s Courses" %prog_name
        while s not in lines[i]:
            i += 1
        #find the first index with a course code
        i = skip_to_next_course(lines, i)
        #read all courses into course objects
        while i < len(lines):
            i = program.read_course(lines, i)
            i = skip_to_next_course(lines, i)
            
            
    def mine_all_courses(self):
        keys = self.program_txts.keys()
        keys.sort()
        for program in keys:
            #self.mine_courses(program)
            try:
                self.mine_courses(program)
            except Exception as e:
                print program, ' ', e.message
            
        
    def is_header(self, line):
        '''(String)->boolean
        Check if the given line is a page header.
        Requires: Programs list has been populated'''        
        if line.strip() in self.programs.keys():
            return True
        return False
    
    def get_DB(self):
        return self.programs

class Searcher(object):
    
    def __init__(self, programs_dict):
        self.DB = programs_dict
        
    def get_programs_list(self):
        return self.DB.keys()
        
    def print_course(self):
        prog_name = raw_input('Program: ')
        while prog_name not in self.DB.keys():
            print 'Given program not in listing'
            prog = raw_input('Program:')
        program = self.DB[prog_name]
        course_code = raw_input('Course Code: ')
        while course_code not in program.courses.keys():
            print 'Given course not in listing'
            course_code = raw_input('Course Code: ')
        course = program.courses[course_code]
        print course
        
    def find_by_distribution(self, dr, programs):
        '''(String, List)->List
        Return a list of the courses in the given programs that
        satisfy the given breadth requirement'''
        course_matches = []
        for p in programs:
            #referred directly to variable = bad form
            courses = self.DB[p].courses.values()
            courses.sort()
            for c in courses:
                curr_dr = c.get_dr()
                prereq = c.get_pre()
                #only returns courses without prereqs
                #curr_dr and dr in curr_dr and 
                if not prereq:
                    course_matches.append(c)
        return course_matches

def print_courses(c_list):
    '''(List)->Nonetype
    Print the given list of courses'''
    
    for c in c_list:
        print '%s %s\n' %(c.get_code(), c.get_name())
        
def input_programs(listing):
    '''Return a list of programs from user input.
    Input is validated against program listing'''
    
    p_list = []
    p = raw_input('Program: ')
    while p != 'done':
        while p not in listing:
            print 'Not a valid program'
            raw_input('Program: ')
        p_list.append(p)
        p = raw_input('Program: ')
    return p_list
        
if __name__ == '__main__':
    DB = Cal_DB("UTCourseCalendar1213.txt")
    DB.populate_progs_list("Sponsoring Department/College/Program Office")
    DB.populate_program_txts()
    DB.mine_all_courses()
    s = Searcher(DB.get_DB())
    matches = s.find_by_distribution('SOC SCI', ['Political Science'])
    print_courses(matches)
    #DB.source.read_until("Arts and Science Programs & Courses")
    #for i in range(8):
        #DB.source.next_line()
    #DB.read_course()